package com.bojiu.lawsearch.lawsearch.wxzf;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

public class WXZFUtil {
	private static Logger logger = LoggerFactory.getLogger(WXZFUtil.class);
	private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
	private static final Random RANDOM = new SecureRandom();

	/**
	 * 创建map用于生成xml 注：默认签名类型为为md5
	 *
	 * @param nonce_str        随机字符串
	 * @param sign             签名
	 * @param openid           openid
	 * @param body             商品描述
	 * @param out_trade_no     商户订单号
	 * @param total_fee        标价金额
	 * @param spbill_create_ip 终端ip
	 * @param notify_url       通知地址
	 * @param trade_type       交易类型
	 *                         JSAPI--JSAPI支付（或小程序支付）、NATIVE--Native支付、APP--app支付，MWEB--H5支付
	 * @param attach           附加数据
	 * @param device_info      设备号
	 * @param limit_pay        限制支付方式
	 * @param product_id       商品id
	 *
	 * @return
	 * @throws Exception
	 */
	public static Map<String, String> createDataMap(String nonce_str, String sign, String openid,
			String body, String out_trade_no, int total_fee, String spbill_create_ip, String notify_url,
			String trade_type, String attach, String device_info, String limit_pay, String product_id)
			throws Exception {
		Map<String, String> map = new HashMap<String, String>();
		map.put("appid", WXZFConstance.GZ_APPID);
		map.put("mch_id", WXZFConstance.MCH_ID);
		map.put("nonce_str", nonce_str);
		map.put("sign", sign);
		map.put("sign_type", "MD5");
		if ("JSAPI".equals(trade_type)) {
			map.put("openid", openid);
		}
		map.put("body", body);
		map.put("out_trade_no", out_trade_no);
		map.put("total_fee", String.valueOf(total_fee));
		map.put("spbill_create_ip", spbill_create_ip);
		map.put("notify_url", notify_url);
		map.put("trade_type", trade_type);
		map.put("attach", attach);
		map.put("device_info", device_info);
		map.put("limit_pay", limit_pay);
		if ("NATIVE".equals(trade_type)) {
			map.put("product_id", product_id);
		}
		map.put("sign", generateSignature(map));
		return map;
	}

	/**
	 * 创建再次签名map
	 *
	 * @param time      时间戳
	 * @param noncestr  随机数
	 * @param prepay_id 预支付id
	 * @return
	 * @throws Exception
	 */
	public static Map<String, String> createZFData(String time, String noncestr, String prepay_id)
			throws Exception {
		Map<String, String> map = new HashMap<String, String>();
		map.put("appId", WXZFConstance.GZ_APPID);
		map.put("timeStamp", time);
		map.put("nonceStr", noncestr);
		map.put("signType", "MD5");
		map.put("package", "prepay_id=" + prepay_id);
		String paySign = MD5("appId=" + WXZFConstance.GZ_APPID + "&nonceStr=" + noncestr + "&package=prepay_id=" + prepay_id
				+ "&signType=MD5&timeStamp=" + time + "&key=" + WXZFConstance.KEY);
		map.put("paySign", paySign);
		return map;
	}

	/**
	 * 创建查询订单map
	 *
	 * @param appid    appid
	 * @param orderNum 商家订单号
	 * @param wxNum    微信订单号
	 * @param noncestr 随机数
	 * @return
	 * @throws Exception
	 */
	public static Map<String, String> createSelectData(String appid, String orderNum, String wxNum, String noncestr)
			throws Exception {
		Map<String, String> map = new HashMap<String, String>();
		map.put("appid", appid);
		map.put("mch_id", WXZFConstance.MCH_ID);
		// 微信订单号跟商户订单号二选一 微信订单号优先
		if (!StringUtils.isEmpty(wxNum)) {
			map.put("transaction_id", wxNum);
		} else {
			map.put("out_trade_no", orderNum);
		}
		map.put("nonce_str", noncestr);
		map.put("sign_type", "MD5");
		map.put("sign", generateSignature(map));
		return map;
	}

	/**
	 * 创建通知返回map
	 *
	 * @param returnCode
	 * @param returnMsg
	 * @return
	 * @throws Exception
	 */
	public static Map<String, String> createInfromData(String returnCode, String returnMsg) throws Exception {
		Map<String, String> map = new HashMap<String, String>();
		map.put("return_code", "<![CDATA[" + returnCode + "]]>");
		map.put("return_msg", "<![CDATA[" + returnMsg + "]]>");
		return map;
	}

	/**
	 * 创建扫码回调后返回的map
	 *
	 * @param return_code
	 * @param appid
	 * @param nonce_str   随机数
	 * @param prepay_id   预支付id
	 * @param result_code
	 * @return
	 * @throws Exception
	 */
	public static Map<String, String> createNativeBakData(String return_code, String appid, String nonce_str,
			String prepay_id, String result_code) throws Exception {
		Map<String, String> map = new HashMap<String, String>();
		map.put("return_code", return_code);
		map.put("appid", appid);
		map.put("mch_id", WXZFConstance.MCH_ID);
		map.put("nonce_str", nonce_str);
		map.put("prepay_id", prepay_id);
		map.put("result_code", result_code);
		map.put("sign", generateSignature(map));
		return map;
	}

	/**
	 *  创建申请退款map
	 * @param appid
	 * @param nonce_str  随机数
	 * @param transaction_id   微信订单号
	 * @param out_trade_no     商户订单号
	 * @param out_refund_no    退款单号
	 * @param total_fee   订单金额
	 * @param refund_fee   退款金额
	 * @param notify_url   结果通知链接
	 *
	 * @return
	 * @throws Exception
	 */
	public static Map<String, String> createRefundData(String appid, String nonce_str, String transaction_id,
			String out_trade_no, String out_refund_no,String total_fee,String refund_fee,String notify_url) throws Exception {
		Map<String, String> map = new HashMap<String, String>();
		map.put("appid", appid);
		map.put("mch_id", WXZFConstance.MCH_ID);
		map.put("nonce_str", nonce_str);
		map.put("sign_type", "MD5");
		if(!StringUtils.isEmpty(transaction_id)) {
			map.put("transaction_id", transaction_id);
		}else {
			map.put("out_trade_no", out_trade_no);
		}
		map.put("out_refund_no", out_refund_no);
		map.put("total_fee", total_fee);
		map.put("refund_fee", refund_fee);
		map.put("notify_url", notify_url);
		map.put("sign", generateSignature(map));
		return map;
	}

	public static Map<String, String> createTransfersData(String openid, String outTradeNo,String amount,String desc,String ip) throws Exception {
		Map<String, String> map = new HashMap<>();
		map.put("mch_appid", WXZFConstance.GZ_APPID);
		map.put("mchid", WXZFConstance.MCH_ID);
		map.put("nonce_str", WXZFUtil.generateNonceStr());
		map.put("sign_type", "MD5");
		map.put("openid", openid);
		map.put("amount", amount);
		map.put("desc", desc);
		// 	NO_CHECK：不校验真实姓名
		//  FORCE_CHECK：强校验真实姓名
		map.put("check_name", "NO_CHECK");
        map.put("partner_trade_no", outTradeNo);
        map.put("spbill_create_ip", ip);
		map.put("sign", generateSignature(map));
		return map;
	}

	/**
	 * 扫码支付模式一生成二维码规则
	 *
	 * @param appid
	 * @param time_stamp
	 * @param nonce_str
	 * @param product_id
	 * @return
	 * @throws Exception
	 */
	public static String createNativeModelOneUrl(String appid, String time_stamp, String nonce_str, String product_id)
			throws Exception {
		Map<String, String> map = new HashMap<String, String>();
		map.put("appid", appid);
		map.put("mch_id", WXZFConstance.MCH_ID);
		map.put("time_stamp", time_stamp);
		map.put("nonce_str", nonce_str);
		map.put("product_id", product_id);
		String url = "weixin://wxpay/bizpayurl?appid=" + appid + "&mch_id=" + WXZFConstance.MCH_ID + "&nonce_str="
				+ nonce_str + "&product_id=" + product_id + "&time_stamp=" + time_stamp + "&sign="
				+ generateSignature(map);
		return url;
	}

	/**
	 * 获取随机字符串 Nonce Str
	 *
	 * @return String 随机字符串
	 */
	public static String generateNonceStr() {
		char[] nonceChars = new char[32];
		for (int index = 0; index < nonceChars.length; ++index) {
			nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
		}
		return new String(nonceChars);
	}

	/**
	 * 生成签名. 注意，默认使用md5
	 *
	 * @param data     待签名数据
	 * @return 签名
	 */
	public static String generateSignature(final Map<String, String> data) throws Exception {
		Map<String, String> sortParams = new TreeMap<String, String>(data);

//		Set<String> keySet = data.keySet();
//		String[] keyArray = keySet.toArray(new String[keySet.size()]);
//		// 将参数名按照ASC码排序
//		Arrays.sort(keyArray);
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String, String> entry : sortParams.entrySet()) {
			String k = entry.getKey();
			// 传送的sign参数不参与签名
			if (k.equals(WXZFConstance.FIELD_SIGN)) {
				continue;
			}
			if (data.get(k).trim().length() > 0) // 参数值为空，则不参与签名
				sb.append(k).append("=").append(data.get(k).trim()).append("&");
		}
		sb.append("key=").append(WXZFConstance.KEY);
		return MD5(sb.toString()).toUpperCase();
	}

	/**
	 * 生成 MD5
	 *
	 * @param data 待处理数据
	 * @return MD5结果
	 */
	public static String MD5(String data) throws Exception {
		MessageDigest md = MessageDigest.getInstance("MD5");
		byte[] array = md.digest(data.getBytes("UTF-8"));
		StringBuilder sb = new StringBuilder();
		for (byte item : array) {
			sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
		}
		return sb.toString().toUpperCase();
	}

	/**
	 * 将Map转换为XML格式的字符串
	 *
	 * @param data Map类型数据
	 * @return XML格式的字符串
	 * @throws Exception
	 */
	public static String mapToXml(Map<String, String> data) throws Exception {
		org.w3c.dom.Document document = WXPayXmlUtil.newDocument();
		org.w3c.dom.Element root = document.createElement("xml");
		document.appendChild(root);
		for (String key : data.keySet()) {
			String value = data.get(key);
			if (value == null) {
				value = "";
			}
			value = value.trim();
			org.w3c.dom.Element filed = document.createElement(key);
			filed.appendChild(document.createTextNode(value));
			root.appendChild(filed);
		}
		TransformerFactory tf = TransformerFactory.newInstance();
		Transformer transformer = tf.newTransformer();
		DOMSource source = new DOMSource(document);
		transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		StringWriter writer = new StringWriter();
		StreamResult result = new StreamResult(writer);
		transformer.transform(source, result);
		String output = writer.getBuffer().toString(); // .replaceAll("\n|\r", "");
		try {
			writer.close();
		} catch (Exception ex) {
		}
		return output;
	}

	/**
	 * 处理 HTTPS API返回数据，转换成Map对象。return_code为SUCCESS时，验证签名。
	 *
	 * @param xmlStr API返回的XML格式数据
	 * @return Map类型数据
	 * @throws Exception
	 */
	public static Map<String, String> processResponseXml(String xmlStr) throws Exception {
		String RETURN_CODE = "return_code";
		String return_code;
		Map<String, String> respData = xmlToMap(xmlStr);
		if (respData.containsKey(RETURN_CODE)) {
			return_code = respData.get(RETURN_CODE);
		} else {
			throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
		}

		if (return_code.equals(WXZFConstance.FAIL)) {
			return respData;
		} else if (return_code.equals(WXZFConstance.SUCCESS)) {
			if (isResponseSignatureValid(respData)) {
				logger.debug("签名验证一致");
				return respData;
			} else {
				throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
			}
		} else {
			throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
		}
	}

	/**
	 * 处理微信扫码回调时提交的数据
	 *
	 * @param xmlStr
	 * @return
	 * @throws Exception
	 */
	public static Map<String, String> nativeModelOneResponseXml(String xmlStr) throws Exception {
		Map<String, String> respData = xmlToMap(xmlStr);
		if (isResponseSignatureValid(respData)) {
			logger.debug("签名验证一致");
			return respData;
		} else {
			throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
		}
	}

	/**
	 * XML格式字符串转换为Map
	 *
	 * @param strXML XML字符串
	 * @return XML数据转换后的Map
	 * @throws Exception
	 */
	public static Map<String, String> xmlToMap(String strXML) throws Exception {
		try {
			Map<String, String> data = new HashMap<String, String>();
			DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
			InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
			org.w3c.dom.Document doc = documentBuilder.parse(stream);
			doc.getDocumentElement().normalize();
			NodeList nodeList = doc.getDocumentElement().getChildNodes();
			for (int idx = 0; idx < nodeList.getLength(); ++idx) {
				Node node = nodeList.item(idx);
				if (node.getNodeType() == Node.ELEMENT_NODE) {
					org.w3c.dom.Element element = (org.w3c.dom.Element) node;
					data.put(element.getNodeName(), element.getTextContent());
				}
			}
			try {
				stream.close();
			} catch (Exception ex) {
				// do nothing
			}
			return data;
		} catch (Exception ex) {
			logger.warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(),
					strXML);
			throw ex;
		}

	}

	/**
	 * 判断xml数据的sign是否有效，必须包含sign字段，否则返回false。
	 *
	 * @param reqData 向wxpay post的请求数据
	 * @return 签名是否有效
	 * @throws Exception
	 */
	public static boolean isResponseSignatureValid(Map<String, String> reqData) throws Exception {
		// 返回数据的签名方式和请求中给定的签名方式是一致的
		return isSignatureValid(reqData);
	}

	/**
	 * 判断签名是否正确，必须包含sign字段，否则返回false。
	 *
	 * @param data     Map类型数据
	 * @return 签名是否正确
	 * @throws Exception
	 */
	public static boolean isSignatureValid(Map<String, String> data) throws Exception {
		if (!data.containsKey(WXZFConstance.FIELD_SIGN)) {
			return false;
		}
		String sign = data.get(WXZFConstance.FIELD_SIGN);
		return generateSignature(data).equals(sign);
	}

	/**
	 * 获取真实ip
	 *
	 * @param request
	 * @return
	 */
	public static String getIpAddress(HttpServletRequest request) {
		String ip = request.getHeader("x-forwarded-for");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		if (ip.contains(",")) {
			return ip.split(",")[0];
		} else {
			return ip;
		}
	}
}
